﻿using System;
using System.Text;

using System.Collections.Specialized;
using System.Web;

namespace AMT.IdentityModel.OAuth
{
	/// <summary>
	/// Handles WRAP requests and responses with a STS.
	/// </summary>
	/// <typeparam name="TResultingToken">The type of token to request from the STS</typeparam>
	/// <typeparam name="TClaimToken">The type of token to send to the STS as part of the request</typeparam>
	public class WRAPHandler<TResultingToken, TClaimToken>
		where TResultingToken : System.IdentityModel.Tokens.SecurityToken
		where TClaimToken : System.IdentityModel.Tokens.SecurityToken
	{
		#region Public constants
		public const string WRAPVersion = "WRAPv0.9";
		#endregion

		#region Private constants
		// TODO: seems like a common const; defined elsewhere?
		private const string HTTP_AUTHORIZATION_TAG = "Authorization";

		private const string WRAP_Scope_Label = "wrap_scope";
		private const string WRAP_Name_Label = "wrap_name";
		private const string WRAP_Password_Label = "wrap_password";
		private const string WRAP_Assertion_Label = "wrap_assertion";
		private const string WRAP_Assertion_Format_Label = "wrap_assertion_format";
		// TODO: remove wrap_access_token?  Isn't only needed by parser and header builder?
		private const string WRAP_Access_Token_Label = "wrap_access_token";
		#endregion

		#region Public properties
		// TODO: does spec say scope is always valid URI or URL?
		public string Scope { get; set; }
		// TODO: consider: Issuers maybe != STS?
		public Uri STSUri { get; set; }

		public TClaimToken ClaimToken { get; set; }
		#endregion

		#region Private properties
		private TResultingToken ResultingToken { get; set; }
		#endregion

		#region Public constructors
		public WRAPHandler(Uri stsUri, string scope)
		{
			VerifyTokenTypesAreSupported();

			STSUri = stsUri;
			Scope = scope;
		}
		#endregion

		#region Public methods
		public void PopulateHttpAuthenticationHeader(System.Net.WebHeaderCollection headers)
		{
			if (null != ResultingToken)
			{
				string headerValue = string.Format("WRAP access_token=\"{0}\"", ResultingToken.ToString());
				headers.Set(HTTP_AUTHORIZATION_TAG, headerValue);
			}
			// TODO: consider exception when ResultingToken not set?
		}

		public TResultingToken RetrieveTokenFromSTS()
		{
			VerifyReadyForPost();

			ResultingToken = null;

			byte[] responseBytes = RequestTokenFromSTS();
			// TODO: consider moving to a SWT decoder class
			if (0 <= responseBytes.Length)
			{
				/* Expected response format is
				 *	wrap_access_token=<token>&<other wrap params>
				 *		<token>:	UrlEncoded token result from STS
				 *		<other wrap params>: e.g., wrap_access_token_expires_in; optional?
				 */
				string response = Encoding.UTF8.GetString(responseBytes);
				string[] parts = response.Split('&');
				string[] tokenParts = parts[0].Split('=');
				// Verify split resulted in just 2 parts - wrap_access_token & the token
				System.Diagnostics.Debug.Assert(2 == tokenParts.Length);
				// Verify that wrap_access_token is head of first part
				System.Diagnostics.Debug.Assert(WRAP_Access_Token_Label.Equals(tokenParts[0]));
				string swtToken = HttpUtility.UrlDecode(tokenParts[1]);

				ResultingToken = new Microsoft.IdentityModel.OAuth.SimpleWebToken(swtToken, true) as TResultingToken;
			}
			return ResultingToken;
		}
		#endregion


		#region Private methods
		private NameValueCollection BuildWrapPostValues()
		{
			NameValueCollection nv = new NameValueCollection();
			// WRAP Scope is required in both claims methods
			nv.Add(WRAP_Scope_Label, Scope);

			// Add WRAP parameters based on claim type
			if (typeof(TClaimToken).Equals(typeof(SWTUserPasswordClaimToken)))
			{
				SWTUserPasswordClaimToken claimToken = ClaimToken as SWTUserPasswordClaimToken;
				System.Diagnostics.Debug.Assert(null != claimToken);
				nv.Add(WRAP_Name_Label, claimToken.UserName);
				nv.Add(WRAP_Password_Label, claimToken.Password);
			}
			else if (typeof(TClaimToken).Equals(typeof(SWTUserSymmetricKeyClaimToken)))
			{
				SWTUserSymmetricKeyClaimToken claimToken = ClaimToken as SWTUserSymmetricKeyClaimToken;
				System.Diagnostics.Debug.Assert(null != claimToken);
				// TODO: force SWT or parameterize?
				nv.Add(WRAP_Assertion_Format_Label, "SWT");
				nv.Add(WRAP_Assertion_Label, TokenFactory.CreateToken(claimToken.UserName, claimToken.SymmetricKey));
			}
			else if (typeof(TClaimToken).Equals(typeof(SAML20UserPasswordClaimToken)))
			{
				SAML20UserPasswordClaimToken claimToken = ClaimToken as SAML20UserPasswordClaimToken;
				System.Diagnostics.Debug.Assert(null != claimToken);
				// REMOVE: SAML claims already config'd by c'tor?
				nv.Add(WRAP_Assertion_Format_Label, "SAML");
				nv.Add(WRAP_Assertion_Label, claimToken.Assertion.ToString());
			}
			else
			{
				throw new InvalidOperationException(string.Format("Unknown ClaimToken type, {0}", typeof(TClaimToken).ToString()));
			}

			return nv;
		}


		private byte[] RequestTokenFromSTS()
		{
			byte[] responseBytes = null;
			using (System.Net.WebClient client = new System.Net.WebClient())
			{
				try
				{
					client.BaseAddress = STSUri.AbsoluteUri;
					responseBytes = client.UploadValues(WRAPVersion, "POST", BuildWrapPostValues());
				}
				catch (Exception )
				{
					// TODO: any oppty to handle excps here?
					throw;
				}
			}
			return responseBytes;
		}

	
		private void VerifyReadyForPost()
		{
			if (null == STSUri || string.IsNullOrEmpty(Scope))
			{
				throw new InvalidOperationException("STSUri or Scope is invalid");
			}
			// Protect against someone switching to string or other (which would invalidate the verification)
			System.Diagnostics.Debug.Assert(typeof(Uri).Equals(STSUri.GetType()));

			if (null == ClaimToken)
			{
				throw new InvalidOperationException("ClaimToken is invalid");
			}
			// TODO: consider: validate UserName and Password or SymmetricKey here?
		}

		// This dictionary maintains the allowed mappings between input and return token types.  For example,
		//	Azure ACS will return a SimpleWebToken when provided with SWTUserxxxxxClaimToken
		private static System.Collections.Generic.Dictionary<Type, Type[]> supportedTokens = new System.Collections.Generic.Dictionary<Type,Type[]>() {
			{typeof(SAML20UserPasswordClaimToken), new Type[] { 
				typeof(Microsoft.IdentityModel.OAuth.SimpleWebToken) }
			},

			{typeof(SWTUserPasswordClaimToken), new Type[] { 
				typeof(Microsoft.IdentityModel.OAuth.SimpleWebToken)}
			},

			{typeof(SWTUserSymmetricKeyClaimToken), new Type[] {
				typeof(Microsoft.IdentityModel.OAuth.SimpleWebToken) }
			}
		};

		private void VerifyTokenTypesAreSupported()
		{
			bool combinationIsSupported = false;

			// Verify token combination is supported
			foreach (Type inputTokenType in supportedTokens.Keys)
			{
				if (typeof(TClaimToken).Equals(inputTokenType))
				{
					// The input token type is known; is the expected return type in the input's map?
					foreach (Type resultTokenType in supportedTokens[inputTokenType])
					{
						if (typeof(TResultingToken).Equals(resultTokenType))
						{
							combinationIsSupported = true;
						}
					}
				}
			}

			if (!combinationIsSupported)
			{
				throw new NotSupportedException(string.Format("The resultant token type {0} is not supported for input token type {1}.",
					typeof(TResultingToken).ToString(), typeof(TClaimToken).ToString()));
			}

			// REMOVE:
			//// Verify the output or resulting token type is supported
			//if (!typeof(Microsoft.IdentityModel.OAuth.SimpleWebToken).Equals(typeof(TResultingToken)))
			//{
			//    throw new NotSupportedException(string.Format("The token type {0} is not supported.", typeof(TResultingToken).ToString()));
			//}

			//// Verify the input or claims token type is supported
			//if (!typeof(SWTUserPasswordClaimToken).Equals(typeof(TClaimToken))
			//    && !typeof(SWTUserSymmetricKeyClaimToken).Equals(typeof(TClaimToken)))
			//{
			//    throw new NotSupportedException(string.Format("The token type {0} is not supported.", typeof(TClaimToken).ToString()));
			//}
		}
		#endregion

	}
}
